package chat;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * This class defines a message as a set of byte buffers.
 * 
 * One message is :
 * <ul>
 * <li>
 * the message type as an int
 * </li>
 * <li>
 * the identity of the sender as an int
 * </li>
 * <li>
 * the sequence number of sender as an int
 * </li>
 * <li>
 * the message size as an int
 * </li>
 * <li>
 * the data as a serialized object
 * </li>
 * </ul>
 * 
 * @author chris
 * @author Denis Conan
 * 
 */
public class Message {
	private ByteBuffer[] messageBuffers;
	private SocketChannel theChannel;
	private int messSize;
	private int messIdentity;
	private int messSeqNumber;
	private int messType;
	private boolean debugState = false;
	private ReadMessageStatus readState;

	public Message(final SocketChannel chan) {
		theChannel = chan;
		messageBuffers = new ByteBuffer[2];
		messageBuffers[0] = ByteBuffer.allocate(Integer.SIZE * 4 / Byte.SIZE);
		readState = ReadMessageStatus.ReadUnstarted; // not yet started to read
	}

	public void close() throws IOException {
		theChannel.close();
	}

	public int getContentSize() {
		return messageBuffers[1].capacity();
	}

	public int getType() {
		return messType;
	}

	public int getIdentity() {
		return messIdentity;
	}

	public int getSeqNumber() {
		return messSeqNumber;
	}

	public long write() throws IOException {
		flip();
		return theChannel.write(messageBuffers);
	}

	public void flip() {
		messageBuffers[0].flip();
		messageBuffers[1].flip();
	}

	public void clear() {
		messageBuffers[0].clear();
		messageBuffers[1].clear();
	}

	public void resetReadMessage() {
		messageBuffers[0].clear();
		messageBuffers[1] = null;
		readState = ReadMessageStatus.ReadUnstarted;
	}

	public int setMessage(final int type, final int identity,
			final int seqNumber, final Serializable data) throws IOException {
		ByteArrayOutputStream bo = new ByteArrayOutputStream();
		ObjectOutputStream oo = new ObjectOutputStream(bo);
		oo.writeObject(data);
		oo.close();
		int size = bo.size();
		messType = type;
		messageBuffers[0].clear();
		messageBuffers[0].putInt(messType);
		messageBuffers[0].putInt(identity);
		messageBuffers[0].putInt(seqNumber);
		messageBuffers[0].putInt(size);

		messageBuffers[1] = ByteBuffer.allocate(size);
		messageBuffers[1].put(bo.toByteArray());
		bo.close();
		return size;
	}

	/* return readState */
	public ReadMessageStatus readMessage() throws IOException {
		int recvSize;

		if (readState == ReadMessageStatus.ReadUnstarted) {
			messageBuffers[0].clear();
			readState = ReadMessageStatus.ReadHeaderStarted;
		}
		if (readState == ReadMessageStatus.ReadDataCompleted) {
			clear();
			readState = ReadMessageStatus.ReadHeaderStarted;
		}
		if (readState == ReadMessageStatus.ReadHeaderStarted) {
			if (messageBuffers[0].position() < messageBuffers[0].capacity() - 1) {
				recvSize = theChannel.read(messageBuffers[0]);
				if (debugState) {
					System.out.println("	Received       : " + recvSize);
				}
				if (recvSize == 0) {
					return readState;
				}
				if (recvSize < 0) {
					readState = ReadMessageStatus.ChannelClosed;
					close();
					return readState;
				}
				if (messageBuffers[0].position() < messageBuffers[0].capacity() - 1) {
					return readState;
				}
			}
			messageBuffers[0].flip();
			if (debugState) {
				System.out.println("Position and limit : "
						+ messageBuffers[0].position() + " "
						+ messageBuffers[0].limit());
			}
			messType = messageBuffers[0].getInt();
			messIdentity = messageBuffers[0].getInt();
			messSeqNumber = messageBuffers[0].getInt();
			messSize = messageBuffers[0].getInt();
			if (debugState) {
				System.out
						.println("Message type, identity, seq. number and size : "
								+ messType
								+ " "
								+ messIdentity
								+ " "
								+ messSeqNumber + " " + messSize);
			}
			readState = ReadMessageStatus.ReadHeaderCompleted;
		}
		if (readState == ReadMessageStatus.ReadHeaderCompleted) {
			if (messageBuffers[1] == null
					|| messageBuffers[1].capacity() != messSize) {
				messageBuffers[1] = ByteBuffer.allocate(messSize);
			}
			readState = ReadMessageStatus.ReadDataStarted;
		}
		if (readState == ReadMessageStatus.ReadDataStarted) {
			if (messageBuffers[1].position() < messageBuffers[1].capacity() - 1) {
				recvSize = theChannel.read(messageBuffers[1]);
				if (debugState) {
					System.out.println("	Received       : " + recvSize);
				}
				if (recvSize == 0) {
					return readState;
				}
				if (recvSize < 0) {
					close();
					readState = ReadMessageStatus.ChannelClosed;
					return readState;
				}
			}
			if (debugState) {
				System.out.println("Position and capacity : "
						+ messageBuffers[1].position() + " "
						+ messageBuffers[1].capacity());
			}
			if (messageBuffers[1].position() >= messageBuffers[1].capacity() - 1) {
				readState = ReadMessageStatus.ReadDataCompleted;
			}
		}
		return readState;
	}

	public Object getData() throws IOException {
		Object res = null;
		if (readState == ReadMessageStatus.ReadDataCompleted) {
			try {
				messageBuffers[1].flip();
				ByteArrayInputStream bi = new ByteArrayInputStream(
						messageBuffers[1].array());
				ObjectInputStream oi = new ObjectInputStream(bi);
				res = oi.readObject();
				oi.close();
				bi.close();
			} catch (ClassNotFoundException ce) {
				ce.printStackTrace();
			}
		}
		return res;
	}

	public void printDebug(String msg) {
		System.out.println(">>> " + msg);

		for (int i = 0; i < messageBuffers.length; i++) {
			ByteBuffer buffer = (ByteBuffer) messageBuffers[i];

			System.out.println(">>> " + " position = " + buffer.position()
					+ ", limit = " + buffer.limit() + ", capacity = "
					+ buffer.capacity());
		}
	}
}
